/*
 * Copyright (c) 2009 Bogong (www.bogong.dk)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#include <windows.h>
#include <stdio.h>
#include <celog.h>
#include "common.h"

// Maximum buffer size of a single record
#define MAX_LOG_ENTRY_DATA_SIZE    1024

// A single CELog entry
typedef struct
{
	BYTE Data[MAX_LOG_ENTRY_DATA_SIZE];
	DWORD dwTime;
	WORD wID;
	WORD wLen;
}
CELOG_ENTRY, *PCELOG_ENTRY;

// Timer frequency of the device which is used to calculate the time stamps. 1 means
// uninitialized (not zero to avoid divide by zero errors) - if the first entry in the 
// log file does not contain a timer frequency we will output an error.
static DWORD g_dwFreq = 1;


// Convert time stamp from log file to a string containing hours, minutes, seconds,
// milliseconds and microseconds since the celog.dll was loaded into memory.
static void GetLogEntryTimeString(
    wchar_t *szBuf,         // String buffer to write time string into
	DWORD dwMaxLen,         // Max length of szBuf
    DWORD dwTime            // Timestamp from CELog file
    )
{
    __int64 iVal;
    DWORD dwHours, dwMinutes, dwSeconds, dwMilli, dwMicro;

    iVal = (__int64) dwTime * 1000000;
    iVal = iVal / g_dwFreq;

    dwMicro   = (DWORD)((iVal / 1) % 1000);
    dwMilli   = (DWORD)((iVal / 1000) % 1000);
    dwSeconds = (DWORD)((iVal / 1000000) % 60);
    dwMinutes = (DWORD)((iVal / 60000000) % 60);
    dwHours   = (DWORD)((iVal / ((__int64)3600000000)));

	swprintf_s(szBuf, dwMaxLen, L"%02d:%02d:%02d.%03d.%03d", dwHours, dwMinutes, dwSeconds, dwMilli, dwMicro);
}


// Process a single log entry. We only handle two types, the debug message
// and log marker that contains the timer frequency used to generate the time
// stamps.
static BOOL ProcessLogEntry(
	wchar_t *pszLine,   // Output string where log entry is written 
	DWORD dwMaxLen,     // Max length of pszLine
    WORD wID,           // Log entry ID, one of the CELID_* values
    DWORD dwTime,       // Timestamp, or 0 if no timestamp
    WORD wLen,          // Length of log entry data
    PBYTE pData         // Pointer to log entry data
    )
{
	// Debug message
    if (wID == CELID_DEBUG_MSG)
    {
        wchar_t szTime[MAX_PATH];
        PCEL_DEBUG_MSG pcel = (PCEL_DEBUG_MSG) pData;
        DWORD dwLen = wcslen(pcel->szMessage);
        DWORD dwOffset = 0;

		// Remove any trailing cariage returns or line feeds
		while ((pcel->szMessage[dwLen - 1] == '\r') || (pcel->szMessage[dwLen - 1] == '\n'))
        {
            pcel->szMessage[--dwLen] = '\0';
        }

		// Remove any leading cariage returns or line feeds
		while ((pcel->szMessage[dwOffset] == '\r') || (pcel->szMessage[dwOffset] == '\n'))
        {
            dwOffset++;
        }

        GetLogEntryTimeString(szTime, _countof(szTime), dwTime);
		swprintf_s(pszLine, dwMaxLen, L"%s: PID=%08X TID=%08X: %s", szTime, pcel->pid, pcel->tid, &pcel->szMessage[dwOffset]);
		return TRUE;
    }

	// Clock frequency (this should be the first record in the file)
	else if (wID == CELID_LOG_MARKER)
	{
        PCEL_LOG_MARKER pcel = (PCEL_LOG_MARKER) pData;
        g_dwFreq = pcel->dwFrequency;
    }
	return FALSE;
}


// Read the next entry in the CELog and save it into pEntry
static BOOL ReadLogEntry(
    HANDLE hFile,               // Open handle to celog file
    PCELOG_ENTRY pEntry         // Pointer to an entry that will be filled with data
    )
{
	DWORD  dwHeader;
	DWORD  dwRead;
	WORD   wLenRead;

	ASSERT(pEntry);
	ASSERT(hFile != INVALID_HANDLE_VALUE);

	// Clear result structure
	memset(pEntry, 0, sizeof(CELOG_ENTRY));

	// Read log entry header
	if ((!ReadFile(hFile, &dwHeader, sizeof(dwHeader), &dwRead, NULL)) || (dwRead != sizeof(dwHeader)))
	{
		return FALSE;
	}

	// Check for time stamp
	if (dwHeader & 0x80000000) 
	{
		if ((!ReadFile(hFile, &pEntry->dwTime, sizeof(pEntry->dwTime), &dwRead, NULL)) || (dwRead != sizeof(pEntry->dwTime))) 
		{
			return FALSE;
		}
	}

	// Get size of log entry data
	pEntry->wLen = (WORD)(dwHeader & 0xFFFF);            

	// Get log entry ID
	pEntry->wID = (WORD)((dwHeader >> 16) & 0x7FFF);

	// Actual space taken in file
	wLenRead = (pEntry->wLen + 3) & 0xFFFC;                 

	// Read an extra DWORD if flagged
	if (pEntry->wID == CELID_FLAGGED) 
	{
		wLenRead += 4;
	}

	// Check if log entry can fit into our buffer
	if (wLenRead > MAX_LOG_ENTRY_DATA_SIZE) 
	{
		wprintf(L"WARNING: CELog (.clg) record with length %u exceeds max length %u\n", wLenRead, MAX_LOG_ENTRY_DATA_SIZE);
		return FALSE;
	}

	// Check timer frequency
	if ((g_dwFreq == 1) && (pEntry->wID != CELID_LOG_MARKER)) 
	{
		g_dwFreq = 1193180; // Initialize to CEPC's 1.19MHz clock.
		wprintf(L"WARNING! Timer frequency unknown. Times below are probably inaccurate.\n");
	}

	// If entry is flagged, handle specially
	if (pEntry->wID == CELID_FLAGGED) 
	{
		DWORD dwFlagInfo;
		// Read flag info
		if ((!ReadFile(hFile, &dwFlagInfo, sizeof(dwFlagInfo), &dwRead, NULL)) || (dwRead != sizeof(dwFlagInfo)))
		{
			return FALSE;
		}
		pEntry->wID = (WORD)(dwFlagInfo >> 16);
		wLenRead -= sizeof(DWORD);
	}
	
	// Read log entry data
	if ((!ReadFile(hFile, pEntry->Data, wLenRead, &dwRead, NULL)) || (dwRead != wLenRead))
	{
		return FALSE;
	}
	return TRUE;
}


// Process the log file. Iterate through all log entries in the file.
void ProcessLogFile(
    HANDLE hFile,                   // Handle to open CELog file
    PCELOG_OUTPUT_FUNCTION pFunc,   // Callback function - will be called for each debug message found in the log
    void *pArg                      // User argument that will be passed to pFunc 
    )
{
    PCELOG_ENTRY pEntry = (PCELOG_ENTRY)LocalAlloc(LMEM_FIXED, sizeof(CELOG_ENTRY));

	if (pEntry != NULL) 
	{
		wchar_t szLine[MAX_PATH];

		// Process one log entry at a time until we reach the end of the file
		while (1) 
		{
			if (ReadLogEntry(hFile, pEntry))
			{
				if (ProcessLogEntry(szLine, _countof(szLine), pEntry->wID, pEntry->dwTime, pEntry->wLen, pEntry->Data))
				{
					pFunc(szLine, pArg);
				}
			}
			else
			{
				break;
			}
		}
	    LocalFree(pEntry);
    }
}
